package cn.sciento.transfer.app.service.impl;

import com.alibaba.fastjson.JSON;
import cn.sciento.core.domain.Page;
import io.choerodon.mybatis.pagehelper.PageHelper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
import org.apache.commons.collections4.MapUtils;
import org.apache.ibatis.session.TransactionIsolationLevel;
import cn.sciento.core.message.MessageAccessor;
import cn.sciento.mybatis.DynamicSqlMapper;
import cn.sciento.transfer.api.dto.DataCheckHandleMsgDTO;
import cn.sciento.transfer.api.dto.DataCheckResultDTO;
import cn.sciento.transfer.api.dto.UpdateRowRecordDTO;
import cn.sciento.transfer.infra.constant.Constants;
import cn.sciento.transfer.infra.data.check.mapping.MappedIndex;
import cn.sciento.transfer.infra.data.check.mapping.MappedRow;
import cn.sciento.transfer.infra.data.check.store.DataStoreService;
import cn.sciento.transfer.infra.datasource.mybatis.DynamicSqlMapperTemplate;
import cn.sciento.transfer.infra.datasource.sql.FieldBuilder;
import cn.sciento.transfer.infra.datasource.sql.SqlBuilder;
import cn.sciento.transfer.infra.datasource.sql.SqlUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.task.AsyncTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

@Service
public class DataCheckService {
  private static final Logger LOGGER = LoggerFactory.getLogger(DataCheckService.class);
  
  private static final String SQL_TENANT_LIMIT_PREFIX = " and tenant_id=";
  
  private DataStoreService dataStoreService;
  
  @Autowired
  private DynamicSqlMapperTemplate dynamicSqlMapperTemplate;
  
  @Autowired
  @Qualifier("transferDataCheckExecutor")
  private AsyncTaskExecutor transferDataCheckExecutorPool;
  
  @Autowired
  @Qualifier("transferDataCheckSaveFileExecutor")
  private AsyncTaskExecutor transferDataCheckSaveFileExecutorPool;
  
  @Autowired
  public DataCheckService(DataStoreService dataStoreService) {
    this.dataStoreService = dataStoreService;
  }
  
  public DataCheckResultDTO check(String masterDsCode, String masterServiceCode, String masterDbCode, String masterTable, String slaveDsCode, String slaveServiceCode, String slaveDbCode, String slaveTable, Long tenantId) {
    String masterFileName = masterTable + "_" + UUID.randomUUID().toString().replace("-", "");
    String salveFileName = slaveTable + "_" + UUID.randomUUID().toString().replace("-", "");
    DataCheckResultDTO dataCheckResultDTO = new DataCheckResultDTO();
    DataCheckHandleMsgDTO dataCheckHandleMsgDTO = new DataCheckHandleMsgDTO();
    dataCheckHandleMsgDTO.setSourceTable(masterTable);
    dataCheckHandleMsgDTO.setTargetTable(slaveTable);
    dataCheckResultDTO.setDataCheckHandleMsgDTO(dataCheckHandleMsgDTO);
    if (this.checkDataStructUpdated(masterDsCode, masterServiceCode, masterTable, slaveDsCode, slaveServiceCode, slaveTable, tenantId, dataCheckHandleMsgDTO)) {
      return dataCheckResultDTO;
    } else {
      try {
        CountDownLatch sync = new CountDownLatch(2);
        this.asyncTransferTableDataToFile(masterDsCode, masterServiceCode, masterDbCode, masterTable, masterFileName, tenantId, sync);
        this.asyncTransferTableDataToFile(slaveDsCode, slaveServiceCode, slaveDbCode, slaveTable, salveFileName, tenantId, sync);

        try {
          sync.await();
        } catch (InterruptedException var29) {
          LOGGER.error("some unkonw error happens", var29);
        }

        Map<Long, MappedIndex> masterIndex = new HashMap();
        Map<Long, MappedIndex> slaveIndex = new HashMap();
        Map<Long, MappedIndex> slaveCurrentBlockIndex = null;
        Long startOffset = 0L;
        MappedIndex testIndex = new MappedIndex(-1L, new byte[16], 0L);
        int blockSize = testIndex.calLength() * 1000;

        Map masterCurrentBlockIndex;
        while((masterCurrentBlockIndex = this.dataStoreService.mappedBlockIndexToMap(masterFileName, new DataStoreService.IndexPredicate<Long, MappedIndex>() {
          public Long findKey(MappedIndex mappedIndex) {
            return mappedIndex.getId();
          }

          public MappedIndex findValue(MappedIndex mappedIndex) {
            return mappedIndex;
          }
        }, startOffset, blockSize)) != null) {
          masterCurrentBlockIndex.putAll(masterIndex);
          slaveCurrentBlockIndex = this.dataStoreService.mappedBlockIndexToMap(salveFileName, new DataStoreService.IndexPredicate<Long, MappedIndex>() {
            public Long findKey(MappedIndex mappedIndex) {
              return mappedIndex.getId();
            }

            public MappedIndex findValue(MappedIndex mappedIndex) {
              return mappedIndex;
            }
          }, startOffset, blockSize);
          if (slaveCurrentBlockIndex == null) {
            slaveCurrentBlockIndex = slaveIndex;
          } else {
            ((Map)slaveCurrentBlockIndex).putAll(slaveIndex);
          }

          startOffset = startOffset + (long)blockSize;
          this.compareUpdateBlock(masterCurrentBlockIndex, masterFileName, (Map)slaveCurrentBlockIndex, salveFileName, dataCheckResultDTO);
          masterIndex.clear();
          masterIndex.putAll(masterCurrentBlockIndex);
          slaveIndex.clear();
          slaveIndex.putAll((Map)slaveCurrentBlockIndex);
        }

        this.compareUpdateBlock(masterIndex, masterFileName, slaveIndex, salveFileName, dataCheckResultDTO);
        Map<Long, MappedIndex> allLeftSlaveIndex = this.dataStoreService.mappedBlockIndexToMap(salveFileName, new DataStoreService.IndexPredicate<Long, MappedIndex>() {
          public Long findKey(MappedIndex mappedIndex) {
            return mappedIndex.getId();
          }

          public MappedIndex findValue(MappedIndex mappedIndex) {
            return mappedIndex;
          }
        }, startOffset, 0);
        if (allLeftSlaveIndex != null) {
          slaveIndex.putAll(allLeftSlaveIndex);
        }

        List masterInsertIndexList = masterIndex.values().stream().map((item) -> {
          MappedRow mappedRow = this.dataStoreService.searchRowByOffset(item.getOffset(), masterFileName);
          Map row = (Map)JSON.parseObject(mappedRow.getBody(), Map.class);
          return row;
        }).collect(Collectors.toList());
        dataCheckResultDTO.getMasterInsertList().addAll(masterInsertIndexList);
        List slaveInsertIndexList = slaveIndex.values().stream().map((item) -> {
          MappedRow mappedRow = this.dataStoreService.searchRowByOffset(item.getOffset(), salveFileName);
          Map row = (Map)JSON.parseObject(mappedRow.getBody(), Map.class);
          return row;
        }).collect(Collectors.toList());
        dataCheckResultDTO.getMasterDeleteList().addAll(slaveInsertIndexList);
        dataCheckHandleMsgDTO.setDiffRowCount(dataCheckResultDTO.getUpdateRows().size() + dataCheckResultDTO.getMasterInsertList().size() + dataCheckResultDTO.getMasterDeleteList().size());
        DataCheckResultDTO var25 = dataCheckResultDTO;
        return var25;
      } finally {
        this.dataStoreService.deleteFile(masterFileName);
        this.dataStoreService.deleteFile(salveFileName);
      }
    }
  }

  private boolean checkDataStructUpdated(String masterDsCode, String masterServiceCode, String masterTable, String slaveDsCode, String slaveServiceCode, String slaveTable, Long tenantId, DataCheckHandleMsgDTO dataCheckHandleMsgDTO) {
    String rawSqSlice = "";
    if (tenantId != null) {
      rawSqSlice = " and tenant_id=" + tenantId;
    }
    String finalRawSqSlice = rawSqSlice;
    Page<Map<String, Object>> firstRowOfMaster = (Page<Map<String, Object>>)this.dynamicSqlMapperTemplate.doExecute(masterDsCode, masterServiceCode, TransactionIsolationLevel.NONE, dynamicSqlMapper -> {
          PageHelper.startPage(0, 1);
          return (Page)dynamicSqlMapper.selectList(SqlBuilder.buildSelectAllColumn(null, masterTable, finalRawSqSlice));
        });
    if (!CollectionUtils.isEmpty(firstRowOfMaster.getContent())) {
      dataCheckHandleMsgDTO.setSourceRowCount((int)firstRowOfMaster.getTotalElements());
      dataCheckHandleMsgDTO.setSourceTableFields(new ArrayList(((Map)firstRowOfMaster.getContent().get(0)).keySet()));
    } 
    String finalRawSqSlice1 = rawSqSlice;
    Page<Map<String, Object>> firstRowOfSlave = (Page<Map<String, Object>>)this.dynamicSqlMapperTemplate.doExecute(slaveDsCode, slaveServiceCode, TransactionIsolationLevel.NONE, dynamicSqlMapper -> {
          PageHelper.startPage(0, 1);
          return (Page)dynamicSqlMapper.selectList(SqlBuilder.buildSelectAllColumn(null, slaveTable, finalRawSqSlice1));
        });
    if (!CollectionUtils.isEmpty(firstRowOfSlave.getContent())) {
      dataCheckHandleMsgDTO.setTargetRowCount((int)firstRowOfSlave.getTotalElements());
      dataCheckHandleMsgDTO.setTargetTableFields(new ArrayList(((Map)firstRowOfSlave.getContent().get(0)).keySet()));
    } 
    if (!CollectionUtils.isEmpty(dataCheckHandleMsgDTO.getSourceTableFields()) && 
      !CollectionUtils.isEmpty(dataCheckHandleMsgDTO.getTargetTableFields())) {
      List<String> missField = new ArrayList<>();
      for (String field : dataCheckHandleMsgDTO.getSourceTableFields()) {
        if (!dataCheckHandleMsgDTO.getTargetTableFields().contains(field)) {
          missField.add(field);
        }
      } 
      dataCheckHandleMsgDTO.setMissingFields(missField);
      if (!CollectionUtils.isEmpty(missField)) {
        dataCheckHandleMsgDTO.setMsg(MessageAccessor.getMessage("info.table_struct_update").getDesc());
        return true;
      } 
    } 
    return false;
  }
  
  private void compareUpdateBlock(Map<Long, MappedIndex> masterIndex, String masterFileName, Map<Long, MappedIndex> slaveIndex, String slaveFileName, DataCheckResultDTO dataCheckResultDTO) {
    if (MapUtils.isEmpty(masterIndex) || MapUtils.isEmpty(slaveIndex)) {
      return;
    }
    List<UpdateRowRecordDTO> updateRowRecordDTOList = new ArrayList<>();
    Iterator<Long> masterCursor = masterIndex.keySet().iterator();
    while (masterCursor.hasNext()) {
      Long key = masterCursor.next();
      if (slaveIndex.get(key) == null) {
        continue;
      }
      if (!Arrays.equals(((MappedIndex)masterIndex.get(key)).getMd5Digit(), ((MappedIndex)slaveIndex.get(key)).getMd5Digit())) {
        UpdateRowRecordDTO updateRowRecordDTO = new UpdateRowRecordDTO();
        MappedRow mappedRow = this.dataStoreService.searchRowByOffset(Long.valueOf(((MappedIndex)masterIndex.get(key)).getOffset()), masterFileName);
        Map<String, Object> masterUpdatedRow = JSON.parseObject(mappedRow.getBody(), Map.class, new com.alibaba.fastjson.parser.Feature[0]);
        updateRowRecordDTO.setMasterUpdateRow(masterUpdatedRow);
        MappedRow slaveMappedRow = this.dataStoreService.searchRowByOffset(((MappedIndex) slaveIndex.get(key)).getOffset(), slaveFileName);
        Map<String, Object> slaveUpdatedRow = JSON.parseObject(slaveMappedRow.getBody(), Map.class, new com.alibaba.fastjson.parser.Feature[0]);
        updateRowRecordDTO.setSlaveUpdateRow(slaveUpdatedRow);
        updateRowRecordDTOList.add(updateRowRecordDTO);
        masterCursor.remove();
        slaveIndex.remove(key);
        continue;
      } 
      masterCursor.remove();
      slaveIndex.remove(key);
    } 
    dataCheckResultDTO.getUpdateRows().addAll(updateRowRecordDTOList);
  }
  
  private void asyncTransferTableDataToFile(String dsCode, String serviceCode, String dbCode, String table, String fileName, Long tenantId, CountDownLatch countDownLatch) {
    String rawSqSlice = "";
    if (tenantId != null) {
      rawSqSlice = " and tenant_id=" + tenantId;
    }
    String finalRawSqSlice = rawSqSlice;
    this.transferDataCheckSaveFileExecutorPool.execute(() -> {
      this.dynamicSqlMapperTemplate.doExecute(dsCode, serviceCode, TransactionIsolationLevel.REPEATABLE_READ, (dynamicSqlMapper) -> {
        try {
          PageHelper.startPage(0, Constants.DEFAULT_PAGE_SIZE);
          Page<Map<String, Object>> zeroPage = (Page)dynamicSqlMapper.selectList(SqlBuilder.buildSelectAllColumn((List)null, table, finalRawSqSlice));
          this.savePageDataToFile(dsCode, serviceCode, dbCode, table, zeroPage, fileName);

          for(int page = 1; page < zeroPage.getTotalPages(); ++page) {
            PageHelper.startPage(page, 1000);
            Page<Map<String, Object>> currentPage = (Page)dynamicSqlMapper.selectList(SqlBuilder.buildSelectAllColumn((List)null, table, finalRawSqSlice));
            this.savePageDataToFile(dsCode, serviceCode, dbCode, table, currentPage, fileName);
          }
        } finally {
          countDownLatch.countDown();
        }

        return null;
      });
    });
  }
  
  private void savePageDataToFile(String dsCode, String serviceCode, String dbCode, String table, Page<Map<String, Object>> currentPage, String fileName) {
    if (CollectionUtils.isEmpty(currentPage.getContent())) {
      return;
    }
    List<String> keyField = SqlUtils.getTablePrimaryKey(table, dsCode, dbCode, serviceCode);
    Assert.isTrue((!CollectionUtils.isEmpty(keyField) && keyField.size() == 1), "NOT SUPPORTED PRIMARY KEY");
    String key = keyField.get(0);
    List<MappedRow> mappedRows = (List<MappedRow>)currentPage.getContent().stream().map(item -> {
          MappedRow mappedRow = new MappedRow(JSON.toJSONBytes(item));
          mappedRow.setId(Long.parseLong(item.get(FieldBuilder.formatFieldToColumn(key)).toString()));
          return mappedRow;
        }).collect(Collectors.toList());
    this.dataStoreService.batchSaveRow(mappedRows, fileName);
  }
}
